package core;

import java.util.Collections;
import java.util.EnumMap;
import java.util.Map;

/**
 * Class that will hold Amounts, consisting of a unit type and size
 */
public class Amount extends ModelObject
{
	/**
	 * Data for serializing this project
	 */
	private static final long serialVersionUID = 8908265688196430374L;
	/** unit specifies which enumerated Unit type the Amount object holds
	 * @see Unit
	 */
	private Unit unit;
	/** size specifies the float value of the unit type, e.g. 3.2 fluid oz */
	private float size;
	
	/** Returns a new Amount object which the give parameters
	 * 
	 * @param unit	the enumerated Unit type, see additional notes
	 * @param size	the float value associated with the unit type
	 * @see Unit
	 */
	public Amount(Unit _unit, float _size)
	{
		assert(_size > -1);
		assert(_unit != null);
		if(_unit == Unit.count)
		{
			// size must be whole number
			assert(_size == Math.floor(_size));
		}
		unit = _unit;
		size = _size;
	}
	
	public boolean isValidAmount()
	{
		if(size < 0)
			return false;
		if(unit == Unit.count)
                {
			if(size != Math.floor(size))
				return false;
                }
                else
                {
                    if(size == 0)
                        return false;
                }
                
		return true;
	}
	
	/** Allows the user to customize the size
	 * 
	 * @param size	the new size of the Amount object
	 * @return 		True if success, False if failure
	 * @throws IllegalArgumentException
	 */
	public boolean setSize(float _size) throws IllegalArgumentException
	{
		assert(_size > -1);
		if(_size < 0)
			throw new IllegalArgumentException();
		this.size = _size;
		return true;
	}
	
	/** Returns the size
	 * 
	 * @return the size stored in the Amount object
	 */
	public float getSize()
	{
		return this.size;
	}
	
	/** Allows the user to set the units on the amount
	 * 
	 * @param unit the new Unit type
	 * @return True if success, False if failure
	 * @throws IllegalArgumentException
	 */
	public boolean setUnit(Unit _unit) throws IllegalArgumentException
	{
		assert(_unit != null);
		
		if(_unit == Unit.count && size != Math.floor(size))
			throw new IllegalArgumentException();
		this.unit = _unit;
		return true;
	}
	
	/** Returns the current Unit assigned to the Amount object
	 * 
	 * @return current Unit type
	 * @see Unit
	 */
	public Unit getUnit()
	{
		return this.unit;
	}
	
	/**
	 * Override on the equals operator
	 * @param o the object to be compared to this
	 * @return true if this and o are equal, false if they aren't
	 */
	public boolean equals(Object o) 
	{
		assert(o != null);
		if(this.unit == ((Amount)o).getUnit() && 
		   this.size == ((Amount)o).getSize())
			return true;
		return false;
	}
	
	@Override
	public String toString()
	{
		String newString = "";
		
		newString += size + " " + unit.toString();
		
		return newString;
	}
	
	/**
	 * Add and Amount to this amount.  If the 'toAdd' parameter
	 * is the same units a direct addition will be made.  If the
	 * 'toAdd' parameter is not the same then the amount in
	 * 'toAdd' will be converted to the same units as this 
	 * Amount object. This will throw an IllegalArgumentException()
	 * when the amount toAdd is incompatible with this currently
	 * specified Amount.
	 * 
	 * @param toAdd The amount to add to this object.
	 */
	public void add(Amount toAdd)
	{
		if (toAdd == null) throw new NullPointerException();
		if (!isCompatableAmounts(toAdd)) throw new IllegalArgumentException();
		
		if (this.unit == toAdd.unit)
		{
			this.size += toAdd.size;
		}
		else
		{
			Amount convertedValue = toAdd.convertTo(this.unit);
			
			this.size += convertedValue.size;
		}
	}
	
	/**
	 * This will return a new Amount object converted to
	 * the units specified by the parameter.
	 * 
	 * @param newUnits The units to conver to.
	 * @return The new Amount object containing the converted
	 * 		Amount.
	 */
	public Amount convertTo(Unit newUnits)
	{
		Amount newAmount = new Amount(newUnits, 0.0f);
		float conversionAmount = 0.0f;
		
		//do conversion
		try 
		{
			conversionAmount = Amount.getConversionAmount(this.unit, newUnits);
		} catch (Exception e) 
		{
			System.out.println(e.getMessage());
		}
		
		newAmount.setSize(conversionAmount * this.size);
		
		return newAmount;
	}
	
	/**
	 * This will check to see if Another Amount object is 
	 * the same 'UnitType' as this object.  If it is then
	 * true will be returned otherwise false will be
	 * returned.
	 * 
	 * Compatible will be defined as count is compatible with
	 * count.  Kilo's, pounds, ounces, and grams are compatible.  They
	 * are classified as weights.  And gallons, pints, quarts, ect. are
	 * volumes.
	 * 
	 * @param toCompare The amount to be compared with this object.
	 * @return True if the amounts are compatible. False if they are
	 * 		Not.
	 */
	public boolean isCompatableAmounts(Amount toCompare)
	{
		return (this.unit.getUnitType() == toCompare.unit.getUnitType());
	}
	
	/**
	 * Everything below is setting up the conversion table to convert
	 * from the currently supported units.
	 */
	private static Map<Unit, Map<Unit, Float>> conversionMap;
	static
	{
		Map<Unit, Map<Unit, Float>>creatorMap = new EnumMap<Unit, Map<Unit, Float>>(Unit.class);
		
		//Pounds To 
		Map<Unit, Float> poundsTo = new EnumMap<Unit, Float>(Unit.class);
		poundsTo.put(Unit.ounces, new Float(16.0f));
		poundsTo.put(Unit.grams, new Float(453.592f));
		poundsTo.put(Unit.kilograms, new Float( 0.453592f));
		
		creatorMap.put(Unit.pounds, Collections.unmodifiableMap(poundsTo));
		//-----------------------------------
		
		//Ounces To
		Map<Unit, Float> ouncesTo = new EnumMap<Unit, Float>(Unit.class);
		ouncesTo.put(Unit.pounds, new Float(0.0625f));
		ouncesTo.put(Unit.grams, new Float(28.3495f));
		ouncesTo.put(Unit.kilograms, new Float( 0.0283495f));
		
		creatorMap.put(Unit.ounces, Collections.unmodifiableMap(ouncesTo));
		//-----------------------------------
		
		//gram To
		Map<Unit, Float> gramsTo = new EnumMap<Unit, Float>(Unit.class);
		gramsTo.put(Unit.pounds, new Float(0.00220462f));
		gramsTo.put(Unit.ounces, new Float(0.035274f));
		gramsTo.put(Unit.kilograms, new Float(0.001f));
		
		creatorMap.put(Unit.grams, Collections.unmodifiableMap(gramsTo));
		//-----------------------------------
		
		//Kilo To
		Map<Unit, Float> KiloTo = new EnumMap<Unit, Float>(Unit.class);
		KiloTo.put(Unit.pounds, new Float(2.20462f));
		KiloTo.put(Unit.ounces, new Float( 35.274f));
		KiloTo.put(Unit.grams, new Float(1000.0f));
		
		creatorMap.put(Unit.kilograms, Collections.unmodifiableMap(KiloTo));
		//-----------------------------------
		
		//Volume
		//gal To
		Map<Unit, Float> galTo = new EnumMap<Unit, Float>(Unit.class);
		galTo.put(Unit.quarts, new Float(4.0f));
		galTo.put(Unit.pints, new Float( 8.0f));
		galTo.put(Unit.fluidOunces, new Float(128.0f));
		galTo.put(Unit.liters, new Float(3.78541f));
		
		creatorMap.put(Unit.gallons, Collections.unmodifiableMap(galTo));
		//-----------------------------------
		
		//quart To
		Map<Unit, Float> quartTo = new EnumMap<Unit, Float>(Unit.class);
		quartTo.put(Unit.gallons, new Float(0.25f));
		quartTo.put(Unit.pints, new Float( 2.0f));
		quartTo.put(Unit.fluidOunces, new Float(32.0f));
		quartTo.put(Unit.liters, new Float(0.946353f));
		
		creatorMap.put(Unit.quarts, Collections.unmodifiableMap(quartTo));
		//-----------------------------------
		
		//pints To
		Map<Unit, Float> pintTo = new EnumMap<Unit, Float>(Unit.class);
		pintTo.put(Unit.gallons, new Float(0.125f));
		pintTo.put(Unit.quarts, new Float( 0.5f));
		pintTo.put(Unit.fluidOunces, new Float(16.0f));
		pintTo.put(Unit.liters, new Float(0.473176f));
		
		creatorMap.put(Unit.pints, Collections.unmodifiableMap(pintTo));
		//-----------------------------------

		//FLounces To
		Map<Unit, Float> flOuncesTo = new EnumMap<Unit, Float>(Unit.class);
		flOuncesTo.put(Unit.gallons, new Float(0.0078125f));
		flOuncesTo.put(Unit.quarts, new Float( 0.03125f));
		flOuncesTo.put(Unit.pints, new Float(0.0625f));
		flOuncesTo.put(Unit.liters, new Float(0.0295735f));
		
		creatorMap.put(Unit.fluidOunces, Collections.unmodifiableMap(flOuncesTo));
		//-----------------------------------

		//liters To
		Map<Unit, Float> litersTo = new EnumMap<Unit, Float>(Unit.class);
		litersTo.put(Unit.gallons, new Float(0.264172f));
		litersTo.put(Unit.quarts, new Float(1.05669f));
		litersTo.put(Unit.pints, new Float(2.11338f));
		litersTo.put(Unit.fluidOunces, new Float(33.814f));
		
		creatorMap.put(Unit.liters, Collections.unmodifiableMap(litersTo));
		//-----------------------------------
		
		conversionMap = Collections.unmodifiableMap(creatorMap);
	}
	
	/**
	 * This will take two units.  the first is what the unit is currently in and
	 * then the second will be what units to convert to. This method will then
	 * return the conversion factor between the two.  So you should just multipy
	 * the first unit by the return value to get the second units.
	 * 
	 * @param convertFrom Units to convert from.
	 * @param convertTo Units to convert To.
	 * @return The conversion factor.
	 * @throws Exception If the conversion Unit Types are not compatible and Exception
	 * 		Is thrown.
	 */
	public static float getConversionAmount(Unit convertFrom, Unit convertTo) throws Exception
	{
		if (convertFrom.getUnitType() != convertTo.getUnitType()) 
			throw new Exception("Invalid conversion");
		
		Map<Unit, Float> fromMap = conversionMap.get(convertFrom);
		
		if (fromMap == null) throw new Exception("No conversion for unit: " + convertFrom);
		
		Float returnObject = fromMap.get(convertTo);
		
		if (returnObject == null) throw new Exception("No Conversion for unit: " + convertTo);
		
		return returnObject.floatValue();
	}
}
